#include <iostream>
#include <cstring>
#include <fstream>
#include <vector>
#include <cmath>
#include <ctime>
#include <limits>
#include <iomanip>
#include <stdexcept>

#include "XMLRawParser.h"
#include "Arguments.h"
#include "Algorithm.h"

// ----------------------------------------------------------------------------

namespace gpxtools
{
  class GPXSplit : public XMLParserHandler
  {
  public:
    // -- Constructor -----------------------------------------------------------
    GPXSplit() :
      _arguments("gpxsplit [OPTION].. [FILE]\nSplit the track segments in a GPX-file.\n", "gpxsplit v0.1",
                 "Split the track segments in a GPX-file in multiple track segments based on distance or time and display the resulting GPX-file on standard output."),
      _analyse  (_arguments, true,  'a', "analyse",                         "analyse the file for splitting"),
      _metres   (_arguments, true,  'd', "distance", "METRES",              "split based on a distance", ""),
      _timestamp(_arguments, true,  't', "time",     "YYYY-MM-DD HH:MM:SS", "split based on a timestamp", ""),
      _seconds  (_arguments, true,  's', "seconds",  "SEC",                 "split based on a time difference in seconds", ""),
      _minutes  (_arguments, true,  'm', "minutes",  "MIN",                 "split based on a time difference in minutes", ""),
      _hours    (_arguments, true,  'h', "hours",    "HOURS",               "split based on a time difference in hours", ""),
      _distance(0),
      _duration(0),
      _time(time_t(-1)),
      _xmlParser(this),
      _TrkNr(0),
      _TrkSegNr(0),
      _inTrkSeg(false)
    {
    }

    // -- Deconstructor ---------------------------------------------------------
    virtual ~GPXSplit()
    {
    }

    // -- Parse arguments -------------------------------------------------------
    bool processArguments(int argc, char *argv[])
    {
      std::vector<std::string> filenames;

      if (!_arguments.parse(argc,argv, filenames))
      {
        return false;
      }
      else if (!checkArguments(filenames))
      {
        return false;
      }
      else if (filenames.empty())
      {
        return _xmlParser.parse(std::cin);
      }
      else
      {
        return parseFile(filenames.front());
      }
    }

    // -- Check arguments ---------------------------------------------------------
    bool checkArguments(const std::vector<std::string> &filenames)
    {
      if (filenames.size() > 1)
      {
        std::cerr << "Too many input files."  << std::endl;
        return false;
      }

      if (!_metres.value().empty())
      {
        try
        {
          _distance = std::stoi(_metres.value());

          if (_distance < 0)
          {
            std::cerr << "Invalid value for --" << _metres.longOption() << " : " << _metres.value() << std::endl;
            return false;
          }
        }
        catch(...)
        {
          std::cerr << "Invalid value for --" << _metres.longOption() << " : " << _metres.value() << std::endl;
          return false;
        }
      }

      if (!_timestamp.value().empty())
      {
        struct tm fields = {0};

        if (strptime(_timestamp.value().c_str(), "%Y-%m-%d %H:%M:%S", &fields) == nullptr)
        {
          std::cerr << "Invalid value for --" << _timestamp.longOption() << " : " << _timestamp.value() << std::endl;
          return false;
        }

        _time = mktime(&fields);

        if (_time == (time_t)-1)
        {
          std::cerr << "Invalid time for --" << _timestamp.longOption() << " : " << _timestamp.value() << std::endl;
          return false;
        }
      }

      _duration = 0;
      if (!_seconds.value().empty())
      {
        try
        {
          _duration = std::stoi(_seconds.value());

          if (_duration < 0)
          {
            std::cerr << "Invalid value for --" << _seconds.longOption() << " : " << _seconds.value() << std::endl;
            return false;
          }
        }
        catch(...)
        {
          std::cerr << "Invalid value for --" << _seconds.longOption() << " : " << _seconds.value() << std::endl;
          return false;
        }
      }

      if (!_minutes.value().empty())
      {
        try
        {
          int minutes = std::stoi(_minutes.value());

          if (minutes < 0)
          {
            std::cerr << "Invalid value for --" << _minutes.longOption() << " : " << _minutes.value() << std::endl;
            return false;
          }

          _duration += minutes * 60;
        }
        catch(...)
        {
          std::cerr << "Invalid value for --" << _minutes.longOption() << " : " << _minutes.value() << std::endl;
          return false;
        }
      }

      if (!_hours.value().empty())
      {
        try
        {
          int hours = std::stoi(_hours.value());

          if (hours < 0)
          {
            std::cerr << "Invalid value for --" << _hours.longOption() << " : " << _hours.value() << std::endl;
            return false;
          }

          _duration += hours * 3600;
        }
        catch(...)
        {
          std::cerr << "Invalid value for --" << _hours.longOption() << " : " << _hours.value() << std::endl;
          return false;
        }
      }

      return true;
    }

    // -- Parse a file ----------------------------------------------------------
    bool parseFile(const std::string &filename)
    {
      bool ok =false;

      std::ifstream file(filename);

      if (file.is_open())
      {
        ok = _xmlParser.parse(file);

        file.close();
      }
      else
      {
        std::cerr << "Unable to open: " << filename << std::endl;
      }

      return ok;
    }

  private:
    // -- Types -----------------------------------------------------------------
    enum ChunkType { TEXT, POINT };

    struct Chunk
    {
      Chunk()
      {
        clear(TEXT);
      }

      void clear(ChunkType type)
      {
        _type       = type;
        _text.clear();
        _lat        = 0.0;
        _lon        = 0.0;
        _distance   = -1.0;
        _timeStr.clear();
        _time       = -1.0;
      }

      ChunkType     _type;
      std::string   _text;
      double        _lat;
      double        _lon;
      double        _distance;
      std::string   _timeStr;
      time_t        _time;
    };

    // -- Methods ---------------------------------------------------------------
    static bool getDoubleAttribute(const Attributes &atts, const std::string &key, double &value)
    {
      auto iter = atts.find(key);

      if (iter == atts.end()) return false;

      if (iter->second.empty()) return false;
      try
      {
        value = std::stod(iter->second);

        return true;
      }
      catch(...)
      {
        return false;
      }
    }

    void store(const std::string &text)
    {
      if (_inTrkSeg)
      {
        _current._text.append(text);
      }
      else if (!_analyse.active())
      {
        std::cout << text;
      }
    }

    void processTimeStr(std::string &timeStr, time_t &time)
    {
      time = time_t(-1);

      XMLRawParser::trim(timeStr);

      if (timeStr.size() == 0) return;

      struct tm fields = {0};

      if (strptime(timeStr.c_str(), "%Y-%m-%dT%TZ", &fields) == nullptr) return;

      time = mktime(&fields);
    }

    void analyseChunks()
    {
      int trkPtNr = 0;

      Chunk previous;

      for (auto iter = _chunks.begin(); iter != _chunks.end(); ++iter)
      {
        if (iter->_type == POINT)
        {
          enum {NONE, DISTANCE, TIME, DURATION} reason = NONE;

          trkPtNr++;

          if (_distance > 0 && iter->_distance > _distance)
          {
            reason = DISTANCE;
          }
          else if (_time > 0 && previous._time > 0 && iter->_time > 0 && previous._time <= _time && iter->_time > _time)
          {
            reason = TIME;
          }
          else if (_duration > 0 && previous._time > 0 && iter->_time > 0 && (iter->_time - previous._time) > _duration)
          {
            reason = DURATION;
          }

          if (reason != NONE)
          {
            if (_analyse.active())
            {
              std::cout << "Track: " << _TrkNr << " Segment: " << _TrkSegNr << " Point: " << trkPtNr << " is split on ";
              switch(reason)
              {
                case DISTANCE:
                  std::cout << "distance: " << iter->_distance << "m (" << previous._lat << ',' << previous._lon << ") and (" << iter->_lat << ',' << iter->_lon << ")" << std::endl;
                  break;
                case TIME:
                  std::cout << "time: " << previous._timeStr << " and " << iter->_timeStr << std::endl;
                  break;
                case DURATION:
                  std::cout << "time duration: " << previous._timeStr << " and " << iter->_timeStr << std::endl;
                  break;
              }
            }
            else
            {
              std::cout << _endTrkSeg;
              std::cout << _startTrkSeg;
            }
          }

          previous = *iter;
        }

        if (!_analyse.active())
        {
          std::cout << iter->_text;
        }
      }
    }

  public:
    // -- Callbacks -------------------------------------------------------------
    virtual void unhandled(const std::string &path, const std::string &text)
    {
      store(text);
    }

    virtual void startElement(const std::string &path, const std::string &text, const std::string &, const Attributes &attributes)
    {
      if (path == "/gpx/trk")
      {
        _TrkNr++;
        _TrkSegNr = 0;
      }
      else if (path == "/gpx/trk/trkseg")
      {
        _startTrkSeg = text;

        _chunks.clear();
        _current.clear(TEXT);
        _previous.clear(TEXT);

        _inTrkSeg = true;
        _TrkSegNr++;
      }
      else if (path == "/gpx/trk/trkseg/trkpt")
      {
        if (!_current._text.empty())
        {
          _chunks.push_back(_current);
        }

        _current.clear(TEXT);

        if (getDoubleAttribute(attributes, "lat", _current._lat) &&
            getDoubleAttribute(attributes, "lon", _current._lon))
        {
          _current._type = POINT;

          if (_previous._type == POINT)
          {
            _current._distance = gpx::calcDistance(_previous._lat, _previous._lon, _current._lat, _current._lon);
          }
        }
      }
      else if (path == "/gpx/trk/trkseg/trkpt/time")
      {
        // <time>2012-12-03T13:13:38Z</time>
        _current._timeStr.clear();
      }

      store(text);
    }

    virtual void text(const std::string &path, const std::string &text)
    {
      if (path == "/gpx/trk/trkseg/trkpt/time")
      {
        _current._timeStr.append(text);
      }
      store(text);
    }

    virtual void endElement(const std::string &path, const std::string &text, const std::string &)
    {
      store(text);

      if (path == "/gpx/trk/trkseg")
      {
        _endTrkSeg = text;

        if (!_current._text.empty())
        {
          _chunks.push_back(_current);
        }

        analyseChunks();

        _inTrkSeg = false;
      }
      else if (path == "/gpx/trk/trkseg/trkpt")
      {
        _chunks.push_back(_current);
        if (_current._type == POINT)
        {
          _previous = _current;
        }
        _current.clear(TEXT);
      }
      else if (path == "/gpx/trk/trkseg/trkpt/time")
      {
        processTimeStr(_current._timeStr, _current._time);
      }
    }

  private:
    // -- Members ---------------------------------------------------------------
    arg::Arguments      _arguments;

    arg::Argument       _analyse;
    arg::Argument       _metres;
    arg::Argument       _timestamp;
    arg::Argument       _seconds;
    arg::Argument       _minutes;
    arg::Argument       _hours;

    XMLRawParser        _xmlParser;

    int                 _distance;
    int                 _duration;
    time_t              _time;

    int                 _TrkNr;
    int                 _TrkSegNr;

    bool                _inTrkSeg;
    std::string         _startTrkSeg;
    std::string         _endTrkSeg;
    Chunk               _current;
    Chunk               _previous;
    std::vector<Chunk>  _chunks;

  };
}

// -- Main program ------------------------------------------------------------
int main(int argc, char *argv[])
{
  gpxtools::GPXSplit gpxSplit;

  return gpxSplit.processArguments(argc, argv) ? 0 : 1;
}
